package cn.coder.toolkit;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Path 和 File 互相转换
 * File.toPath()
 * Path.toFile()
 *
 * 注意: Files 里方法返回值为 Stream(8引入的流式操作, 非InputStream这类) 的, 当调用这种方法时, 都需要 try-with-resources 处理, 为防止错误使用, 先置为私有
 *
 * @see Files
 * @see Path
 */
public final class PathKit {

    private PathKit() {}

    private static final int DEFAULT_BUFFER_SIZE = 8192; // 缓冲区大小

    public static boolean exists(Path path) {
        return Files.exists(path);
    }

    public static boolean notExists(Path path) {
        return Files.notExists(path);
    }

    public static boolean isFile(Path path) {
        return Files.isRegularFile(path);
    }

    public static boolean isDirectory(Path path) {
        return Files.isDirectory(path);
    }

    /**
     * 获取文件大小(byte)
     * 文件夹会返回0
     */
    public static long size(Path path) throws IOException {
        return Files.size(path);
    }

    /**
     * 获取文件大小(byte)并转换为合适的单位
     */
    public static String sizeFormat(long sizeInBytes) {
        if (sizeInBytes < 1024) {
            return sizeInBytes + " B";
        }
        double size = sizeInBytes;
        String[] units = { "B", "KB", "MB", "GB", "TB" };
        int unitIndex = 0;
        while (size >= 1024 && unitIndex < units.length - 1) {
            size /= 1024.0;
            unitIndex++;
        }
        return new DecimalFormat("#,##0.##").format(size) + " " + units[unitIndex];
    }

    /**
     * 获取文件大小(byte)并转换为合适的单位
     */
    public static String sizeFormat(Path path) throws IOException {
        return sizeFormat(Files.size(path));
    }

    /**
     * 获取文本文件的总行数(自行确保是文本文件)
     */
    public static long count(Path path) throws IOException {
        try (Stream<String> lines = Files.lines(path)) {
            return lines.count();
        }
    }

    /**
     * 计算文件的MD5
     */
    public static String md5(Path path) throws IOException {
        try (final BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(path))) {
            // 生成 MessageDigest
            MessageDigest instance;
            try {
                instance = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
            // 计算 digest
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int read = stream.read(buffer, 0, DEFAULT_BUFFER_SIZE);
            while (read > -1) {
                instance.update(buffer, 0, read);
                read = stream.read(buffer, 0, DEFAULT_BUFFER_SIZE);
            }
            byte[] digest = instance.digest();
            // 结果转换为 16 进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                String hexStr = Integer.toHexString(b & 0xFF);
                if (hexStr.length() == 1) {
                    sb.append('0');
                }
                sb.append(hexStr);
            }
            return sb.toString();
        }
    }

    /**
     * 删除目录或文件
     */
    public static void delete(Path path) throws IOException {
        if (Files.notExists(path)) {
            return;
        }
        if (Files.isDirectory(path)) {
            // 深度优先遍历文件树, 执行顺序如下
            // preVisitDirectory: 初次进入某目录 Parent 时调用, 调用后开始遍历该目录内的路径(目录和文件), 如果是目录则继续进入(深度优先), 如果是文件则调用 visitFile
            // visitFile: 遍历到的路径是文件时调用
            // visitFileFailed: 遍历到的路径是文件且无法访问文件的信息时调用
            // postVisitDirectory: 遍历完 Parent 内的所有路径(目录和文件)后调用
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.deleteIfExists(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    Files.deleteIfExists(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
            return;
        }
        Files.deleteIfExists(path);
    }

    /**
     * 拷贝目录或文件(Stream)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     */
    public static void copy(Path source, Path target) throws IOException {
        // 创建 target 的父目录
        Files.createDirectories(target.getParent());
        // 判断路径类型决定如何拷贝
        if (Files.isDirectory(source)) {
            // 不直接删除目标目录及其文件, 需要的话自行在拷贝前调用 delete(target);
            Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    // 获取 dir 相对于 source 多出来的部分, 然后拼接到 target 后面
                    // Files.copy 可以拷贝目录(不包含其中文件)
                    Files.copy(dir, target.resolve(source.relativize(dir)));
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.copy(file, target.resolve(source.relativize(file)));
                    return FileVisitResult.CONTINUE;
                }
            });
            return;
        }
        // 单独的 Files.copy() 只能复制文件或单层目录, 不能复制目录内的文件和子目录
        // 单独拷贝文件时默认直接覆盖
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
    }

    /**
     * 移动文件或目录
     */
    public static void move(Path source, Path target) throws IOException {
        // 创建 target 的父目录
        Files.createDirectories(target.getParent());
        // 判断路径类型决定如何移动
        if (Files.isDirectory(source)) {
            // 判断是否在同一个 FileStore(表示存储池、设备(磁盘)、分区、卷、具体文件系统或其它实现特定的文件存储手段)
            if (Files.getFileStore(source).equals(Files.getFileStore(target))) {
                Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
            } else {
                // 完全复制
                copy(source, target);
                // 删除旧的
                delete(source);
            }
            return;
        }
        // 单独的 Files.move() 只能移动文件或单层目录, 不能移动目录内的文件和子目录
        // 单独移动文件时默认直接覆盖
        Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
    }

    /**
     * 重命名文件或目录
     */
    public static void rename(Path path, String name) throws IOException {
        Files.move(path, path.resolveSibling(name));
    }

    /**
     * 创建目录
     */
    public static void mkdirs(Path path) throws IOException {
        Files.createDirectories(path);
    }

    /**
     * 列出当前路径下匹配通配符的 Path
     * list(path, "*.java"), 返回匹配通配符的全部 Path
     * @param glob 通配符
     */
    public static List<Path> list(Path path, String glob) throws IOException {
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, glob)) {
            return StreamSupport.stream(stream.spliterator(), false).collect(Collectors.toList()); // 将 DirectoryStream 转为 Stream
        }
    }

    /**
     * 列出当前路径下满足条件的 Path
     */
    public static List<Path> list(Path path, Predicate<Path> matcher) throws IOException {
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
            return StreamSupport.stream(stream.spliterator(), false).filter(matcher).collect(Collectors.toList()); // 将 DirectoryStream 转为 Stream
        }
    }

    /**
     * 列出当前路径下的全部 Path
     */
    public static List<Path> list(Path path) throws IOException {
        // 看源码得知, Files.newDirectoryStream(path, "*") 等效于 Files.newDirectoryStream(path)
        return list(path, "*");
    }

    /**
     * 搜索当前路径及子路径下全部匹配通配符的 Path
     * @param glob 通配符
     */
    public static List<Path> find(Path path, String glob) throws IOException {
        try (Stream<Path> stream = Files.walk(path)) {
            return stream.parallel().filter(item -> FileSystems.getDefault().getPathMatcher("glob:" + glob).matches(item.getFileName())).collect(Collectors.toList()); // 使用并行流加速操作
        }
    }

    /**
     * 搜索当前路径及子路径下全部满足条件的 Path
     * @param matcher 匹配路径的条件
     * 注意: 如果 Path 的 startsWith 和 endsWith 方法用于判断最前和最后一节目录, 和 String 的同名方法效果是不一样的, 可以使用 path.toString().endsWith(".cat") 来判断
     */
    public static List<Path> find(Path path, Predicate<Path> matcher) throws IOException {
        try (Stream<Path> stream = Files.walk(path)) {
            return stream.parallel().filter(matcher).collect(Collectors.toList()); // 使用并行流加速操作
        }
    }

    /**
     * 搜索当前路径及子路径下全部 Path
     */
    public static List<Path> find(Path path) throws IOException {
        return find(path, "*");
    }

    /**
     * 从字节输入流中读文件
     */
    public static void copy(InputStream in, Path path) throws IOException {
        Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
    }

    /**
     * 写文件到字节输出流中
     */
    public static void copy(Path path, OutputStream out) throws IOException {
        Files.copy(path, out);
    }

    /**
     * 拷贝文件(RandomAccessFile)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     * RandomAccessFile, 支持随机访问, 既可以读取文件内容, 也可以向文件输出数据. 它读写的是文件而不是流
     */
    private static void copyFileWithRandomAccessFile(Path source, Path target) throws IOException {
        Files.createDirectories(target.getParent());
        try (RandomAccessFile in = new RandomAccessFile(source.toFile(), "r");
             RandomAccessFile out = new RandomAccessFile(target.toFile(), "rw")) {
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
        }
    }

    /**
     * 拷贝文件(Stream)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     * 流通常不支持随机访问, 即不能随意定位索引位置, 不能任意跳过或重复读取某段数据
     * </p>
     * BufferedInputStream默认有8192字节的缓存, 在读取单个字节的时候, 缓存才有作用, 能减少IO交互. 读取字节数组的时候, 和InputStream没有区别
     * </p>
     * </p>
     * </p>
     * Stream(BIO,同步阻塞) 和 Channel(NIO,同步非阻塞) 的区别
     * </p>
     * 重要说明, JDK1.4引入了NIO, 且BIO的底层也以NIO的方式进行了部分重写, 所以就文件读写效率来说, 两者的差别已经不大, 最重要的区别还是NIO提供了同步非阻塞的网络编程模型, 这是BIO所没有的
     * </p>
     * 早期的BIO与NIO最重要的区别是数据打包和传输方式, BIO以流的方式处理数据, 一次处理一个字节, 通常相当慢, NIO以块(Buffer)的方式处理数据, 一次处理一块数据(多个字节), 比BIO快很多
     * 应用程序从磁盘取数据有如下两个步骤
     *      1.先由操作系统将磁盘上的数据读到系统内存中, 这一步是操作系统IO, 不由应用程序控制
     *      2.然后应用程序将数据从系统内存读到应用内存中, 这一步就是我们程序中的IO操作
     * 第一步中, 操作系统从磁盘读取数据到系统内存, 一般都是一次读取一块数据(多个字节)
     * BIO和NIO的区别主要存在于第二步中, BIO需要一个字节一个字节地将系统内存中的数据读到应用内存, 而NIO则是从系统内存中一次读取一块数据(多个字节)到应用内存, 效率自然是NIO的更好
     * 再次说明, JDK1.4引入了NIO, 且BIO的底层也以NIO的方式进行了部分重写, 所以就文件读写效率来说, 两者的差别已经不大, 最重要的区别还是NIO提供了同步非阻塞的网络编程模型, 这是BIO所没有的
     * </p>
     * Stream不支持异步, 只能单向传输数据, 不需要结合Buffer使用, 性能较低, Channel与之相反
     * </p>
     * https://zhuanlan.zhihu.com/p/364880932?utm_id=0
     * https://blog.csdn.net/u014453898/article/details/113660547
     */
    private static void copyFileWithStream(Path source, Path target) throws IOException {
        Files.createDirectories(target.getParent());
        try (InputStream in = Files.newInputStream(source);
             OutputStream out = Files.newOutputStream(target)) {
//            in.transferTo(out); // Since 9
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int length;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
        }
    }

    /**
     * 拷贝文件(ByteChannel)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     * 和Stream(BIO)的文件读写效率差不多, 没有经过零拷贝优化, 所以和Stream的区别就是BIO(同步阻塞)和NIO(同步非阻塞)的区别了
     */
    private static void copyFileWithByteChannel(Path source, Path target) throws IOException {
        Files.createDirectories(target.getParent());
        try (SeekableByteChannel in = Files.newByteChannel(source, StandardOpenOption.READ);
             SeekableByteChannel out = Files.newByteChannel(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
            ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
            while (in.read(buffer) != -1) {
                buffer.flip();
                out.write(buffer);
                buffer.clear();
            }
        }
    }

    /**
     * 拷贝文件(FileChannel)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     * 零拷贝是指避免在用户态(User-space)与内核态(Kernel-space)之间来回拷贝数据的技术
     * 利用了操作系统内核的零拷贝优化, 省略了数据在内核空间和用户空间之间的两次拷贝过程
     * 传统非零拷贝复制的数据流转过程: 磁盘-内核空间-用户空间-内核空间-磁盘
     * 使用了零拷贝复制的数据流转过程: 磁盘-内核空间-内核空间-磁盘
     * 零拷贝: https://zhuanlan.zhihu.com/p/364880932
     * 零拷贝: https://zhuanlan.zhihu.com/p/640744637
     */
    private static void copyFileWithFileChannel(Path source, Path target) throws IOException {
        Files.createDirectories(target.getParent());
        try (FileInputStream inputStream = new FileInputStream(source.toFile());
             FileOutputStream outputStream = new FileOutputStream(target.toFile());
             FileChannel in = inputStream.getChannel();
             FileChannel out = outputStream.getChannel()) {
            long count = 0;
            long size = in.size();
            while (count < size) { // 循环支持2G以上文件
                long position = count;
                count += out.transferFrom(in, position, size - position);
            }
        }
    }

    /**
     * 拷贝文件(MappedByteBuffer)
     * </p>
     * Windows Oracle JDK 17 测试结果
     * FillChannel < Stream ≈ ByteChannel ≈ RandomAccessFile
     * MappedByteBuffer, 耗时与多个因素有关
     *      与分片大小有关, 分片越大效率越高, 分片大小最大不能超过Integer.MAX_VALUE, 即2G大小
     *      与文件大小有关, 多个分片时, 越靠后的分片的拷贝的耗时越长, 猜测是越靠后越是要跳过更多的position导致的
     *      与文件类型有关, 如相似大小的mp4格式的视频文件明显比zip文件要快很多
     * Files.copy() 在 Windows 平台是最快的 (条件是文件大小在某个范围内)
     * 综合考虑 Files.copy() 和 FillChannel 比较合适
     * </p>
     * 映射一旦建立，就不依赖于用于创建它的文件通道。特别是关闭通道对映射的有效性没有影响. 导致被印射的文件无法删除
     * 对于大多数操作系统，将文件映射到内存通过比常规 read 方法读取 write 或写入几十KB的数据更昂贵。从性能的角度来看，通常只值得将相对较大的文件映射到内存中
     * </p>
     * Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间
     * 映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间, 同样地, 内核空间对这段区域的修改也直接反映用户空间
     * 正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space)之间拷贝数据, 提高了数据传输的效率
     * 直接内存印射与直接内存(对外内存): https://zhuanlan.zhihu.com/p/640744637
     * 内存印射释放: https://www.jianshu.com/p/eb80c8eceefc
     */
    private static void copyFileWithMappedByteBuffer(Path source, Path target) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Files.createDirectories(target.getParent());
        try (FileInputStream inputStream = new FileInputStream(source.toFile());
             RandomAccessFile output = new RandomAccessFile(target.toFile(), "rw");
             FileChannel in = inputStream.getChannel();
             FileChannel out = output.getChannel()) {

            // 为手动释放内存印射做准备, 做这么多, 本质上就是在调用 Unsafe.getUnsafe().invokeCleaner(buffer);
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            Object unsafe = unsafeField.get(null);
            Method invokeCleaner = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
            // 为手动释放内存印射做准备

            long fileSize = in.size(); // 源文件大小
//            long chunkSize = 1024 * 1024 * 1000; // 分片大小, 100MB
            long chunkSize = Integer.MAX_VALUE; // 分片大小, 100MB
            long chunkCount = fileSize / chunkSize; // 分片数量
            boolean divisible = fileSize % chunkSize == 0; // 是否能被整除
            if (!divisible) {
                chunkCount++;
            }
            for (int i = 0; i < chunkCount; i++) {
                long position = i * chunkSize;
                long size = (i < chunkCount - 1) ? chunkSize : (fileSize - position);
                MappedByteBuffer outBuffer = out.map(FileChannel.MapMode.READ_WRITE, position, size);
                MappedByteBuffer inBuffer = in.map(FileChannel.MapMode.READ_ONLY, position, size);
                outBuffer.put(inBuffer);
                // 手动释放内存印射
                invokeCleaner.invoke(unsafe, outBuffer);
                invokeCleaner.invoke(unsafe, inBuffer);
            }
        }
    }

    public static InputStream newInputStream(Path path) throws IOException {
        return Files.newInputStream(path, StandardOpenOption.READ);
    }

    public static OutputStream newOutputStream(Path path, boolean append) throws IOException {
        return Files.newOutputStream(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING);
    }

    public static BufferedReader newBufferedReader(Path path, Charset charset) throws IOException {
        return Files.newBufferedReader(path, charset);
    }

    public static BufferedReader newBufferedReader(Path path) throws IOException {
        return Files.newBufferedReader(path, StandardCharsets.UTF_8);
    }

    public static BufferedWriter newBufferedWriter(Path path, boolean append, Charset charset) throws IOException {
        return Files.newBufferedWriter(path, charset, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING);
    }

    public static BufferedWriter newBufferedWriter(Path path, boolean append) throws IOException {
        return newBufferedWriter(path, append, StandardCharsets.UTF_8);
    }

    /**
     * 创建读操作的字节通道
     * @see SeekableByteChannel: 可以进行读/写操作,同时还可以通过改变文件位置指针来随机访问文件内容
     */
    public static SeekableByteChannel newReadByteChannel(Path path) throws IOException {
        return Files.newByteChannel(path, EnumSet.of(StandardOpenOption.READ));
    }

    /**
     * 创建写操作的字节通道
     * @see SeekableByteChannel: 可以进行读/写操作,同时还可以通过改变文件位置指针来随机访问文件内容
     */
    public static SeekableByteChannel newWriteByteChannel(Path path, boolean append) throws IOException {
        return Files.newByteChannel(path, EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING));
    }

    public static byte[] readBytes(Path path) throws IOException {
        return Files.readAllBytes(path);
    }

    public static byte[] readBytes(InputStream inputStream) throws IOException {
        // return inputStream.readAllBytes(); // since 9
        try (InputStream in = inputStream;
            ByteArrayOutputStream buffer = new ByteArrayOutputStream(DEFAULT_BUFFER_SIZE)) {
            byte[] data = new byte[DEFAULT_BUFFER_SIZE];
            int nRead;
            while ((nRead = in.read(data)) != -1) {
                buffer.write(data, 0, nRead);
            }
            return buffer.toByteArray();
        }
    }

    public static void writeBytes(Path path, byte[] bytes, boolean append) throws IOException {
        Files.write(path, bytes, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING);
    }

    public static String readString(Path path, Charset charset) throws IOException {
//        return Files.readString(path, charset); // Since 11
//        return new String(Files.readAllBytes(path), charset);
        try (Stream<String> stream = readStringStream(path, charset)) {
            return stream.collect(Collectors.joining(System.lineSeparator()));
        }
    }

    public static String readString(Path path) throws IOException {
        return readString(path, StandardCharsets.UTF_8);
    }

    public static String readString(InputStream inputStream, Charset charset) throws IOException {
        try (Stream<String> stream = readStringStream(inputStream, charset)) {
            return stream.collect(Collectors.joining(System.lineSeparator()));
        }
    }

    public static String readString(InputStream inputStream) throws IOException {
        return readString(inputStream, StandardCharsets.UTF_8);
    }

    public static void writeString(Path path, String string, boolean append, Charset charset) throws IOException {
        // Files.writeString(path, string, charset, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING); // Since 11
        Files.write(path, Collections.singletonList(string), charset, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING);
    }

    public static void writeString(Path path, String string, boolean append) throws IOException {
        writeString(path, string, append, StandardCharsets.UTF_8);
    }

    public static List<String> readStringList(Path path, Charset charset) throws IOException {
//        return Files.readAllLines(path, charset);
        try (Stream<String> stream = readStringStream(path, charset)) {
            return stream.collect(Collectors.toList());
        }
    }

    public static List<String> readStringList(Path path) throws IOException {
        return readStringList(path, StandardCharsets.UTF_8);
    }

    public static List<String> readStringList(InputStream inputStream, Charset charset) throws IOException {
        try (Stream<String> stream = readStringStream(inputStream, charset)) {
            return stream.collect(Collectors.toList());
        }
    }

    public static List<String> readStringList(InputStream inputStream) throws IOException {
        return readStringList(inputStream, StandardCharsets.UTF_8);
    }

    public static void writeStringList(Path path, List<String> list, boolean append, Charset charset) throws IOException {
        Files.write(path, list, charset, StandardOpenOption.WRITE, StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING);
    }

    public static void writeStringList(Path path, List<String> list, boolean append) throws IOException {
        writeStringList(path, list, append, StandardCharsets.UTF_8);
    }

    /**
     * Stream<String> 是懒加载的, 只有调用终端操作（比如 forEach）时才会读取文件, 非常适用于中大文件, 但不要收集成链表, 需要 try-with-resources 处理
     */
    public static Stream<String> readStringStream(Path path, Charset charset) throws IOException {
        return Files.lines(path, charset);
    }

    /**
     * Stream<String> 是懒加载的, 只有调用终端操作（比如 forEach）时才会读取文件, 非常适用于中大文件, 但不要收集成链表, 需要 try-with-resources 处理
     */
    private static Stream<String> readStringStream(Path path) throws IOException {
        return readStringStream(path, StandardCharsets.UTF_8);
    }

    /**
     * Stream<String> 是懒加载的, 只有调用终端操作（比如 forEach）时才会读取文件, 非常适用于中大文件, 但不要收集成链表, 需要 try-with-resources 处理
     */
    public static Stream<String> readStringStream(InputStream inputStream, Charset charset) throws IOException {
        return new BufferedReader(new InputStreamReader(inputStream, charset)).lines();
    }

    /**
     * Stream<String> 是懒加载的, 只有调用终端操作（比如 forEach）时才会读取文件, 非常适用于中大文件, 但不要收集成链表, 需要 try-with-resources 处理
     */
    private static Stream<String> readStringStream(InputStream inputStream) throws IOException {
        return readStringStream(inputStream, StandardCharsets.UTF_8);
    }

}
